/*****************************************************************************
 * Copyright (C) Stacy Curl. All rights reserved.                            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *****************************************************************************/
package picounit.features.test_factory;

import picounit.Lifecycle;
import picounit.features.aggregate_exception.AggregateAssertionFailedError;
import picounit.features.aggregate_exception.Thrower;
import picounit.features.mock_collaborator_injection.CollaboratorsInvocationHandler;
import picounit.features.test_instantiator.Instantiator;
import picounit.features.test_instantiator.Invoker;
import picounit.features.test_instantiator.LifecycleInstantiator;
import picounit.mocker.MockControl;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;

class PicoUnitTestCase<T> extends TestCase {
	private final Class<T> testClass;
	private final Method testMethod;

	private final Instantiator instantiator;
	private final Invoker invoker;
	private final MockControl mockControl;
	private final Thrower thrower;
	private final LifecycleInstantiator lifecycleInstantiator;
	private final TestListener testListener;
	private final CollaboratorsInvocationHandler collaboratorsInvocationHandler;
	private Lifecycle[] lifecyclesSetUp;
	private T testInstance;
	private boolean testInstanceSetUp;

	public PicoUnitTestCase(Class<T> testClass, Method testMethod, Instantiator instantiator,
		Invoker invoker, MockControl mockControl, Thrower thrower,
		LifecycleInstantiator lifecycleInstantiator, TestListener testListener,
		CollaboratorsInvocationHandler collaboratorsInvocationHandler) {

		this.testMethod = testMethod;
		this.testClass = testClass;
		this.instantiator = instantiator;
		this.invoker = invoker;
		this.mockControl = mockControl;
		this.thrower = thrower;
		this.lifecycleInstantiator = lifecycleInstantiator;
		this.testListener = testListener;
		this.collaboratorsInvocationHandler = collaboratorsInvocationHandler;
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////////
	// TestCase
	///////////////////////////////////////////////////////////////////////////////////////////////

	@Override
	public int countTestCases() {
		return 1;
	}

	@Override
	public String getName() {
		return testMethod.getName() + "(" + getDeclaringClassName() + ")"; 
	}
	
	@Override
	public void runBare() throws Throwable {
		int numOfDataDrivenVariations = 1;//signatureFactory.create(testMethod).numMatching();

		for (int dataDrivenIndex = 0; dataDrivenIndex < numOfDataDrivenVariations; dataDrivenIndex++) {
			setUp();
			
			try {
				runTest();
	
				tearDown();
			}
			catch (AssertionFailedError assertionFailedError) {
				tearDown(assertionFailedError);
			}
		}
	}

	@Override
	protected void setUp() throws Exception {
		collaboratorsInvocationHandler.clearMap();

		testListener.testStarted(getDeclaringClassName(), testMethod.getName());

		try {
			lifecyclesSetUp = setUp(getLifecycles());
			
			thrower.clearError();
	
			testInstance = instantiator.instantiate(testClass);
	
			setUp(testInstance);

			mock(testInstance);

			testInstanceSetUp = true;
		}
		catch (Exception exception) {
			testListener.addError(testClass, testMethod);

			throw exception;
		}
	}

	@Override
	protected void runTest() throws Throwable {
		try {
			invoker.invoke(testMethod, testInstance);

			thrower.dispatchError();
		}
		catch (ThreadDeath threadDeath) {
			throw threadDeath;
		}
		catch (AssertionFailedError assertionFailedError) {
			testListener.addFailure(testClass, testMethod);

			throw assertionFailedError;
		}
		catch (InvocationTargetException invocationTargetException) {
			if (invocationTargetException.getTargetException() instanceof AssertionFailedError) {
				testListener.addFailure(testClass, testMethod);
			}
			else {
				testListener.addError(testClass, testMethod);
			}

			throw invocationTargetException.getTargetException();
		}
		catch (Throwable throwable) {
			testListener.addError(testClass, testMethod);

			throw throwable;
		}
	}
	
	@Override
	protected void tearDown() throws Exception {
		try {
			AggregateAssertionFailedError aggregateAssertionFailedError =
				new AggregateAssertionFailedError();

			verify(aggregateAssertionFailedError);

			if (testInstanceSetUp) {
				try {
					tearDown(testInstance);
				}
				catch (AssertionFailedError assertionFailedError) {
					aggregateAssertionFailedError.add(assertionFailedError);
				}
			}

			tearDown(lifecyclesSetUp, aggregateAssertionFailedError);

		}
		finally {
			testListener.testFinished(getDeclaringClassName(), testMethod.getName());
		}
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////////
	// Implementation
	///////////////////////////////////////////////////////////////////////////////////////////////
	
	private void tearDown(AssertionFailedError assertionFailedError) throws Exception {
		try {
			AggregateAssertionFailedError aggregateAssertionFailedError =
				new AggregateAssertionFailedError(assertionFailedError);

			verify(aggregateAssertionFailedError);

			if (testInstanceSetUp) {
				try {
					tearDown(testInstance);
				}
				catch (AssertionFailedError afe) {
					aggregateAssertionFailedError.add(afe);
				}
			}

			tearDown(lifecyclesSetUp, aggregateAssertionFailedError);

		}
		finally {
			testListener.testFinished(getDeclaringClassName(), testMethod.getName());
		}
	}

	private void verify(AggregateAssertionFailedError aggregateAssertionFailedError) {
		try {
			mockControl.verify();
		}
		catch (AssertionFailedError assertionFailedError) {
			aggregateAssertionFailedError.add(assertionFailedError);
		}
	}

	private void mock(T testInstance) throws IllegalArgumentException, IllegalAccessException,
		InvocationTargetException {

		mockControl.reset();
		invoker.invoke("mock", testInstance);
	}

	private Lifecycle[] getLifecycles() throws IllegalArgumentException, InstantiationException,
		IllegalAccessException, ClassNotFoundException, InvocationTargetException {

		return lifecycleInstantiator.instantiateLifecyclesFor(testClass);
	}

	private Lifecycle[] setUp(Lifecycle[] lifecycles) throws IllegalAccessException, InvocationTargetException {
		Lifecycle[] lifecyclesSetUp = new Lifecycle[lifecycles.length];

		for (int index = 0; index < lifecycles.length; index++) {
			setUp(lifecycles[index]);

			lifecyclesSetUp[index] = lifecycles[index];
		}

		return lifecyclesSetUp;
	}

	private void setUp(Object target) throws IllegalAccessException, InvocationTargetException {
		invoker.invoke("setUp", target);
	}

	private void tearDown(Object target) throws IllegalAccessException, InvocationTargetException {
		invoker.invoke("tearDown", target);
	}
	
	private void tearDown(Lifecycle[] lifecycles,
		AggregateAssertionFailedError aggregateAssertionFailedError)
			throws IllegalAccessException, InvocationTargetException {

		for (int index = lifecycles.length - 1; index >= 0; index--) {
			Lifecycle lifecycle = lifecycles[index];

			if (lifecycle != null) {
				try {
					tearDown(lifecycle);
				}
				catch (AssertionFailedError assertionFailedError) {
					aggregateAssertionFailedError.add(assertionFailedError);
				}
			}
		}

		aggregateAssertionFailedError.throwIfContains();
	}

	private String getDeclaringClassName() {
		return testMethod.getDeclaringClass().getName();
	}
}
